home *** CD-ROM | disk | FTP | other *** search
Wrap
/* AutoHotkey Copyright 2003-2007 Chris Mallett (support@autohotkey.com) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. */ #include "stdafx.h" // pre-compiled headers #include "clipboard.h" #include "globaldata.h" // for g_script.ScriptError() and g_ClipboardTimeout #include "application.h" // for MsgSleep() #include "util.h" // for strlcpy() size_t Clipboard::Get(char *aBuf) // If aBuf is NULL, it returns the length of the text on the clipboard and leaves the // clipboard open. Otherwise, it copies the clipboard text into aBuf and closes // the clipboard (UPDATE: But only if the clipboard is still open from a prior call // to determine the length -- see later comments for details). In both cases, the // length of the clipboard text is returned (or the value CLIPBOARD_FAILURE if error). // If the clipboard is still open when the next MsgSleep() is called -- presumably // because the caller never followed up with a second call to this function, perhaps // due to having insufficient memory -- MsgSleep() will close it so that our // app doesn't keep the clipboard tied up. Note: In all current cases, the caller // will use MsgBox to display an error, which in turn calls MsgSleep(), which will // immediately close the clipboard. { // Seems best to always have done this even if we return early due to failure: if (aBuf) // It should be safe to do this even at its peak capacity, because caller // would have then given us the last char in the buffer, which is already // a zero terminator, so this would have no effect: *aBuf = '\0'; UINT i, file_count = 0; BOOL clipboard_contains_text = IsClipboardFormatAvailable(CF_TEXT); BOOL clipboard_contains_files = IsClipboardFormatAvailable(CF_HDROP); if (!(clipboard_contains_text || clipboard_contains_files)) return 0; if (!mIsOpen) { // As a precaution, don't give the caller anything from the clipboard // if the clipboard isn't already open from the caller's previous // call to determine the size of what's on the clipboard (no other app // can alter its size while we have it open). The is to prevent a // buffer overflow from happening in a scenario such as the following: // Caller calls us and we return zero size, either because there's no // CF_TEXT on the clipboard orthere was a problem opening the clipboard. // In these two cases, the clipboard isn't open, so by the time the // caller calls us again, there's a chance (vanishingly small perhaps) // that another app (if our thread were preempted long enough, or the // platform is multiprocessor) will have changed the contents of the // clipboard to something larger than zero. Thus, if we copy that // into the caller's buffer, the buffer might overflow: if (aBuf) return 0; if (!Open()) { // Since this should be very rare, a shorter message is now used. Formerly, it was // "Could not open clipboard for reading after many timed attempts. Another program is probably holding it open." Close(CANT_OPEN_CLIPBOARD_READ); return CLIPBOARD_FAILURE; } if ( !(mClipMemNow = g_clip.GetClipboardDataTimeout(clipboard_contains_files ? CF_HDROP : CF_TEXT)) ) { // v1.0.47.04: Commented out the following that had been in effect when clipboard_contains_files==false: // Close("GetClipboardData"); // Short error message since so rare. // return CLIPBOARD_FAILURE; // This was done because there are situations when GetClipboardData can fail indefinitely. // For example, in Firefox, pulling down the Bookmarks menu then right-clicking "Bookmarks Toolbar // Folder" then selecting "Copy" puts one or more formats on the clipboard that cause this problem. // For details, search the forum for TYMED_NULL. // // v1.0.42.03: For the fix below, GetClipboardDataTimeout() knows not to try more than once // for CF_HDROP. // Fix for v1.0.31.02: When clipboard_contains_files==true, tolerate failure, which happens // as a normal/expected outcome when there are files on the clipboard but either: // 1) zero of them; // 2) the CF_HDROP on the clipboard is somehow misformatted. // If you select the parent ".." folder in WinRar then use the following hotkey, the script // would previously yield a runtime error: //#q:: //Send, ^c //ClipWait, 0.5, 1 //msgbox %Clipboard% //Return Close(); if (aBuf) *aBuf = '\0'; return 0; } // Although GlobalSize(mClipMemNow) can yield zero in some cases -- in which case GlobalLock() should // not be attempted -- it probably can't yield zero for CF_HDROP and CF_TEXT because such a thing has // never been reported by anyone. Therefore, GlobalSize() is currently not called. if ( !(mClipMemNowLocked = (char *)GlobalLock(mClipMemNow)) ) { Close("GlobalLock"); // Short error message since so rare. return CLIPBOARD_FAILURE; } // Otherwise: Update length after every successful new open&lock: // Determine the length (size - 1) of the buffer than would be // needed to hold what's on the clipboard: if (clipboard_contains_files) { if (file_count = DragQueryFile((HDROP)mClipMemNowLocked, 0xFFFFFFFF, "", 0)) { mLength = (file_count - 1) * 2; // Init; -1 if don't want a newline after last file. for (i = 0; i < file_count; ++i) mLength += DragQueryFile((HDROP)mClipMemNowLocked, i, NULL, 0); } else mLength = 0; } else // clipboard_contains_text mLength = strlen(mClipMemNowLocked); if (mLength >= CLIPBOARD_FAILURE) // Can't realistically happen, so just indicate silent failure. return CLIPBOARD_FAILURE; } if (!aBuf) return mLength; // Above: Just return the length; don't close the clipboard because we expect // to be called again soon. If for some reason we aren't called, MsgSleep() // will automatically close the clipboard and clean up things. It's done this // way to avoid the chance that the clipboard contents (and thus its length) // will change while we don't have it open, possibly resulting in a buffer // overflow. In addition, this approach performs better because it avoids // the overhead of having to close and reopen the clipboard. // Otherwise: if (clipboard_contains_files) { if (file_count = DragQueryFile((HDROP)mClipMemNowLocked, 0xFFFFFFFF, "", 0)) for (i = 0; i < file_count; ++i) { // Caller has already ensured aBuf is large enough to hold them all: aBuf += DragQueryFile((HDROP)mClipMemNowLocked, i, aBuf, 999); if (i < file_count - 1) // i.e. don't add newline after the last filename. { *aBuf++ = '\r'; // These two are the proper newline sequence that the OS prefers. *aBuf++ = '\n'; } //else DragQueryFile() has ensured that aBuf is terminated. } // else aBuf has already been terminated upon entrance to this function. } else strcpy(aBuf, mClipMemNowLocked); // Caller has already ensured that aBuf is large enough. // Fix for v1.0.37: Close() is no longer called here because it prevents the clipboard variable // from being referred to more than once in a line. For example: // Msgbox %Clipboard%%Clipboard% // ToolTip % StrLen(Clipboard) . Clipboard // Instead, the clipboard is later closed in other places (search on CLOSE_CLIPBOARD_IF_OPEN // to find them). The alternative to fixing it this way would be to let it reopen the clipboard // by means getting rid of the following lines above: //if (aBuf) // return 0; // However, that has the risks described in the comments above those two lines. return mLength; } ResultType Clipboard::Set(char *aBuf, UINT aLength) //, bool aTrimIt) // Returns OK or FAIL. { // It was already open for writing from a prior call. Return failure because callers that do this // are probably handling things wrong: if (IsReadyForWrite()) return FAIL; if (!aBuf) { aBuf = ""; aLength = 0; } else if (aLength == UINT_MAX) // Caller wants us to determine the length. aLength = (UINT)strlen(aBuf); if (aLength) { if (!PrepareForWrite(aLength + 1)) return FAIL; // It already displayed the error. strlcpy(mClipMemNewLocked, aBuf, aLength + 1); // Copy only a substring, if aLength specifies such. // Only trim when the caller told us to, rather than always if g_script.mIsAutoIt2 // is true, since AutoIt2 doesn't always trim things (e.g. FileReadLine probably // does not trim the line that was read into its output var). UPDATE: This is // no longer needed because I think AutoIt2 only auto-trims when SetEnv is used: //if (aTrimIt) // trim(mClipMemNewLocked); } // else just do the below to empty the clipboard, which is different than setting // the clipboard equal to the empty string: it's not truly empty then, as reported // by IsClipboardFormatAvailable(CF_TEXT) -- and we want to be able to make it truly // empty for use with functions such as ClipWait: return Commit(); // It will display any errors. } char *Clipboard::PrepareForWrite(size_t aAllocSize) { if (!aAllocSize) return NULL; // Caller should ensure that size is at least 1, i.e. room for the zero terminator. if (IsReadyForWrite()) // It was already prepared due to a prior call. Currently, the most useful thing to do // here is return the memory area that's already been reserved: return mClipMemNewLocked; // Note: I think GMEM_DDESHARE is recommended in addition to the usual GMEM_MOVEABLE: // UPDATE: MSDN: "The following values are obsolete, but are provided for compatibility // with 16-bit Windows. They are ignored.": GMEM_DDESHARE if ( !(mClipMemNew = GlobalAlloc(GMEM_MOVEABLE, aAllocSize)) ) { g_script.ScriptError("GlobalAlloc"); // Short error message since so rare. return NULL; } if ( !(mClipMemNewLocked = (char *)GlobalLock(mClipMemNew)) ) { mClipMemNew = GlobalFree(mClipMemNew); // This keeps mClipMemNew in sync with its state. g_script.ScriptError("GlobalLock"); // Short error message since so rare. return NULL; } mCapacity = (UINT)aAllocSize; // Keep mCapacity in sync with the state of mClipMemNewLocked. *mClipMemNewLocked = '\0'; // Init for caller. return mClipMemNewLocked; // The caller can now write to this mem. } ResultType Clipboard::Commit(UINT aFormat) // If this is called while mClipMemNew is NULL, the clipboard will be set to be truly // empty, which is different from writing an empty string to it. Note: If the clipboard // was already physically open, this function will close it as part of the commit (since // whoever had it open before can't use the prior contents, since they're invalid). { if (!mIsOpen && !Open()) // Since this should be very rare, a shorter message is now used. Formerly, it was // "Could not open clipboard for writing after many timed attempts. Another program is probably holding it open." return AbortWrite(CANT_OPEN_CLIPBOARD_WRITE); if (!EmptyClipboard()) { Close(); return AbortWrite("EmptyClipboard"); // Short error message since so rare. } if (mClipMemNew) { bool new_is_empty = false; // Unlock prior to calling SetClipboardData: if (mClipMemNewLocked) // probably always true if we're here. { // Best to access the memory while it's still locked, which is why this temp var is used: // v1.0.40.02: The following was fixed to properly recognize 0x0000 as the Unicode string terminator, // which fixes problems with Transform Unicode. new_is_empty = !mClipMemNewLocked[0] && (aFormat != CF_UNICODETEXT || !mClipMemNewLocked[1]); GlobalUnlock(mClipMemNew); // mClipMemNew not mClipMemNewLocked. mClipMemNewLocked = NULL; // Keep this in sync with the above action. mCapacity = 0; // Keep mCapacity in sync with the state of mClipMemNewLocked. } if (new_is_empty) // Leave the clipboard truly empty rather than setting it to be the // empty string (i.e. these two conditions are NOT the same). // But be sure to free the memory since we're not giving custody // of it to the system: mClipMemNew = GlobalFree(mClipMemNew); else if (SetClipboardData(aFormat, mClipMemNew)) // In any of the failure conditions above, Close() ensures that mClipMemNew is // freed if it was allocated. But now that we're here, the memory should not be // freed because it is owned by the clipboard (it will free it at the appropriate time). // Thus, we relinquish the memory because we shouldn't be looking at it anymore: mClipMemNew = NULL; else { Close(); return AbortWrite("SetClipboardData"); // Short error message since so rare. } } // else we will close it after having done only the EmptyClipboard(), above. // Note: Decided not to update mLength for performance reasons (in case clipboard is huge). // Anyway, it seems rather pointless because once the clipboard is closed, our app instantly // loses sight of how large it is, so the the value of mLength wouldn't be reliable unless // the clipboard were going to be immediately opened again. return Close(); } ResultType Clipboard::AbortWrite(char *aErrorMessage) // Always returns FAIL. { // Since we were called in conjunction with an aborted attempt to Commit(), always // ensure the clipboard is physically closed because even an attempt to Commit() // should physically close it: Close(); if (mClipMemNewLocked) { GlobalUnlock(mClipMemNew); // mClipMemNew not mClipMemNewLocked. mClipMemNewLocked = NULL; mCapacity = 0; // Keep mCapacity in sync with the state of mClipMemNewLocked. } // Above: Unlock prior to freeing below. if (mClipMemNew) mClipMemNew = GlobalFree(mClipMemNew); // Caller needs us to always return FAIL: return *aErrorMessage ? g_script.ScriptError(aErrorMessage) : FAIL; } ResultType Clipboard::Close(char *aErrorMessage) // Returns OK or FAIL (but it only returns FAIL if caller gave us a non-NULL aErrorMessage). { // Always close it ASAP so that it is free for other apps to use: if (mIsOpen) { if (mClipMemNowLocked) { GlobalUnlock(mClipMemNow); // mClipMemNow not mClipMemNowLocked. mClipMemNowLocked = NULL; // Keep this in sync with its state, since it's used as an indicator. } // Above: It's probably best to unlock prior to closing the clipboard. CloseClipboard(); mIsOpen = false; // Even if above fails (realistically impossible?), seems best to do this. // Must do this only after GlobalUnlock(): mClipMemNow = NULL; } // Do this cleanup for callers that didn't make it far enough to even open the clipboard. // UPDATE: DO *NOT* do this because it is valid to have the clipboard in a "ReadyForWrite" // state even after we physically close it. Some callers rely on that. //if (mClipMemNewLocked) //{ // GlobalUnlock(mClipMemNew); // mClipMemNew not mClipMemNewLocked. // mClipMemNewLocked = NULL; // mCapacity = 0; // Keep mCapacity in sync with the state of mClipMemNewLocked. //} //// Above: Unlock prior to freeing below. //if (mClipMemNew) // // Commit() was never called after a call to PrepareForWrite(), so just free the memory: // mClipMemNew = GlobalFree(mClipMemNew); if (aErrorMessage && *aErrorMessage) // Caller needs us to always return FAIL if an error was displayed: return g_script.ScriptError(aErrorMessage); // Seems best not to reset mLength. But it will quickly become out of date once // the clipboard has been closed and other apps can use it. return OK; } HANDLE Clipboard::GetClipboardDataTimeout(UINT uFormat) // Same as GetClipboardData() except that it doesn't give up if the first call to GetClipboardData() fails. // Instead, it continues to retry the operation for the number of milliseconds in g_ClipboardTimeout. // This is necessary because GetClipboardData() has been observed to fail in repeatable situations (this // is strange because our thread already has the clipboard locked open -- presumably it happens because the // GetClipboardData() is unable to start a data stream from the application that actually serves up the data). // If cases where the first call to GetClipboardData() fails, a subsequent call will often succeed if you give // the owning application (such as Excel and Word) a little time to catch up. This is especially necessary in // the OnClipboardChange label, where sometimes a clipboard-change notification comes in before the owning // app has finished preparing its data for subsequent readers of the clipboard. { #ifdef DEBUG_BY_LOGGING_CLIPBOARD_FORMATS // Provides a convenient log of clipboard formats for analysis. static FILE *fp = fopen("c:\\debug_clipboard_formats.txt", "w"); #endif char format_name[MAX_PATH + 1]; // MSDN's RegisterClipboardFormat() doesn't document any max length, but the ones we're interested in certainly don't exceed MAX_PATH. if (uFormat < 0xC000 || uFormat > 0xFFFF) // It's a registered format (you're supposted to verify in-range before calling GetClipboardFormatName()). Also helps performance. *format_name = '\0'; // Don't need the name if it's a standard/CF_* format. else { // v1.0.42.04: // Probably need to call GetClipboardFormatName() rather than comparing directly to uFormat because // MSDN implies that OwnerLink and other registered formats might not always have the same ID under // all OSes (past and future). GetClipboardFormatName(uFormat, format_name, MAX_PATH); // Since RegisterClipboardFormat() is case insensitive, the case might vary. So use stricmp() when // comparing format_name to anything. // "Link Source", "Link Source Descriptor" , and anything else starting with "Link Source" is likely // to be data that should not be attempted to be retrieved because: // 1) It causes unwanted bookmark effects in various versions of MS Word. // 2) Tests show that these formats are on the clipboard only if MS Word is open at the time // ClipboardAll is accessed. That implies they're transitory formats that aren't as essential // or well suited to ClipboardAll as the other formats (but if it weren't for #1 above, this // wouldn't be enough reason to omit it). // 3) Although there is hardly any documentation to be found at MSDN or elsewhere about these formats, // it seems they're related to OLE, with further implications that the data is transitory. // Here are the formats that Word 2002 removes from the clipboard when it the app closes: // 0xC002 ObjectLink >>> Causes WORD bookmarking problem. // 0xC003 OwnerLink // 0xC00D Link Source >>> Causes WORD bookmarking problem. // 0xC00F Link Source Descriptor >>> Doesn't directly cause bookmarking, but probably goes with above. // 0xC0DC Hyperlink if ( !strnicmp(format_name, "Link Source", 11) || !stricmp(format_name, "ObjectLink") || !stricmp(format_name, "OwnerLink") // v1.0.44.07: The following were added to solve interference with MS Outlook's MS Word editor. // If a hotkey like ^F1::ClipboardSave:=ClipboardAll is pressed after pressing Ctrl-C in that // editor (perhaps only when copying HTML), two of the following error dialogs would otherwise // be displayed (this occurs in Outlook 2002 and probably later versions): // "An outgoing call cannot be made since the application is dispatching an input-synchronous call." || !stricmp(format_name, "Native") || !stricmp(format_name, "Embed Source") ) return NULL; } #ifdef DEBUG_BY_LOGGING_CLIPBOARD_FORMATS fprintf(fp, "%04X\t%s\n", uFormat, format_name); // Since fclose() is never called, the program has to exit to close/release the file. #endif HANDLE h; for (DWORD start_time = GetTickCount();;) { // Known failure conditions: // GetClipboardData() apparently fails when the text on the clipboard is greater than a certain size // (Even though GetLastError() reports "Operation completed successfully"). The data size at which // this occurs is somewhere between 20 to 96 MB (perhaps depending on system's memory and CPU speed). if (h = GetClipboardData(uFormat)) // Assign return h; // It failed, so act according to the type of format and the timeout that's in effect. // Certain standard (numerically constant) clipboard formats are known to validly yield NULL from a // call to GetClipboardData(). Never retry these because it would only cause unnecessary delays // (i.e. a failure until timeout). // v1.0.42.04: More importantly, retrying them appears to cause problems with saving a Word/Excel // clipboard via ClipboardAll. if (uFormat == CF_HDROP // This format can fail "normally" for the reasons described at "clipboard_contains_files". || !stricmp(format_name, "OwnerLink")) // Known to validly yield NULL from a call to GetClipboardData(), so don't retry it to avoid having to wait the full timeout period. return NULL; if (g_ClipboardTimeout != -1) // We were not told to wait indefinitely and... if (!g_ClipboardTimeout // ...we were told to make only one attempt, or ... || (int)(g_ClipboardTimeout - (GetTickCount() - start_time)) <= SLEEP_INTERVAL_HALF) //...it timed out. // Above must cast to int or any negative result will be lost due to DWORD type. return NULL; // Use SLEEP_WITHOUT_INTERRUPTION to prevent MainWindowProc() from accepting new hotkeys // during our operation, since a new hotkey subroutine might interfere with // what we're doing here (e.g. if it tries to use the clipboard, or perhaps overwrites // the deref buffer if this object's caller gave it any pointers into that memory area): SLEEP_WITHOUT_INTERRUPTION(INTERVAL_UNSPECIFIED) } } ResultType Clipboard::Open() { if (mIsOpen) return OK; for (DWORD start_time = GetTickCount();;) { if (OpenClipboard(g_hWnd)) { mIsOpen = true; return OK; } if (g_ClipboardTimeout != -1) // We were not told to wait indefinitely... if (!g_ClipboardTimeout // ...and we were told to make only one attempt, or ... || (int)(g_ClipboardTimeout - (GetTickCount() - start_time)) <= SLEEP_INTERVAL_HALF) //...it timed out. // Above must cast to int or any negative result will be lost due to DWORD type. return FAIL; // Use SLEEP_WITHOUT_INTERRUPTION to prevent MainWindowProc() from accepting new hotkeys // during our operation, since a new hotkey subroutine might interfere with // what we're doing here (e.g. if it tries to use the clipboard, or perhaps overwrites // the deref buffer if this object's caller gave it any pointers into that memory area): SLEEP_WITHOUT_INTERRUPTION(INTERVAL_UNSPECIFIED) } }